Описание регистров в SystemVerilog

Перед тем, как описывать память, необходимо научиться описывать отдельные регистры. Регистр — это базовая ячейка памяти, позволяющая хранить состояние, пока на схему подается питание. В современной электронике, регистр чаще всего строится на D-триггерах. В лабораторной работе по АЛУ уже вскользь упоминалось, что как для описания проводов, так и для описания регистров, используется тип logic.

logic reg_name;

../.pic/Basic%20Verilog%20structures/registers/fig_01.drawio.svg

У регистра может быть несколько входов и один выход. Основных входов, без которых не может существовать регистр два: вход данных и вход тактирующего синхроимпульса. На рисунке они обозначены как D и clk. Опциональный вход сигнала сброса (rst) позволяет обнулять содержимое регистра вне зависимости от входных данных и может работать как с тактовым синхроимпульсом (синхронный сброс), так и без него (асинхронный сброс).

Помимо прочего у регистра также может быть входной сигнал разрешения записи (enable), который определяет будут ли записаны данные с входного сигнала данных в регистр или нет, опциональный вход установки (set), позволяющий принудительно выставить значение регистра в единицу.

Выход у регистра один. На рисунке выше он обозначен как Q.

Важно понимать, что названия приведенных портов не являются чем-то высеченным на камне, они просто описывают функциональное назначение. В процессе описания работы регистра вы будете оперировать только над именем регистра, и сигналами, которые подводите к нему.

Поскольку все сигналы в цифровой схеме передаются по цепям, удобно представлять, что к выходу регистра всегда неявно подключен провод, с именем, совпадающим с именем регистра, поэтому вы можете использовать имя регистра в дальнейшей цифровой логике:

../.pic/Basic%20Verilog%20structures/registers/fig_02.drawio.svg

Итак, мы добавили регистр на холст схемы, но как соединить его с какой-то логикой? Предположим, у нас есть сигнал тактового синхроимпульса и данные, которые мы хотим записать:

../.pic/Basic%20Verilog%20structures/registers/fig_03.drawio.svg

Данной схеме соответствует код:

module reg_example(
  input  logic clk,
  input  logic data,
  output logic reg_data
);

  logic reg_name;

endmodule

Очевидно, мы хотим подключить сигнал clk ко входу тактирующего сигнала регистра, вход data ко входу данных, а выход регистра к выходу reg_data:

../.pic/Basic%20Verilog%20structures/registers/fig_04.drawio.svg

Запись в регистр возможна только по фронту тактирующего синхроимпульса. Фронт — это переход сигнала из нуля в единицу (положительный фронт), либо из единицы в ноль (отрицательный фронт).

Описание регистра, а также указание фронта и тактирующего сигнала происходит в конструкции always_ff:

always @(posedge clk)

Далее, внутри данной конструкции необходимо указать, что происходит с содержимым регистра. В нашем случае, происходит запись с входного сигнала data

always @(posedge clk) begin
  reg_name <= data;
end

Обратите внимание на оператор <=. В данном случае, это не знак "меньше либо равно", а оператор неблокирующего присваивания. Существует оператор блокирующего присваивания (=), который меняет способ построения схемы для такого же выражения справа от оператора, однако в данный момент этот оператор останется за рамками курса. Хоть это и плохая практика в обучении, но пока вам надо просто запомнить, что при описании записи в регистр всегда используйте оператор неблокирующего присваивания <=.

Помимо прочего, нам необходимо связать выход схемы с выходом регистра. Это можно сделать уже известным вам оператором непрерывного присваивания assign.

Таким образом, итоговый код описания данной схемы примет вид:

module reg_example(
  input  logic clk,
  input  logic data,
  output logic reg_data
);

  logic reg_name;

  always @(posedge clk) begin
    reg_name <= data;
  end

  assign reg_data = reg_name;

endmodule

Предположим, мы хотим добавить управление записью в регистр через сигналы enable и reset. Это, например, можно сделать следующим образом:

module reg_example(
  input  logic clk,
  input  logic data,
  input  logic reset,
  input  logic enable,
  output logic reg_data
);

  logic reg_name;

  always_ff @(posedge clk) begin
    if(reset) begin
      reg_name <= 1'b0;
    end
    else if(enable) begin
      reg_name <= data;
    end
  end

  assign reg_data = reg_name;

endmodule

Обратите внимание на очередность условий. В первую очередь, мы проверяем условие сброса, и только после этого условие разрешения на запись. Если сперва проверить разрешение на запись, а затем в блоке else описать логику сброса, то регистр не будет сбрасываться в случае, если enable будет равен 1 (запись в регистр будет приоритетней его сброса). Если сброс описать не в блоке else, а в отдельном блоке if, то может возникнуть неопределенное состояние: нельзя однозначно сказать в какой момент придет сигнал reset относительно сигнала enable и что в итоге запишется в регистр. Поэтому при наличии сигнала сброса, остальная логика по записи в регистр должна размещаться в блоке else.

Кроме того, САПР-ы смотрят на паттерн описания элемента схемы, и когда распознают его, реализуют элемент так как задумывал разработчик. Поэтому при описании регистра всегда сперва описывается сигнал сброса (если он используется) и только затем в блоке else описывается вся остальная часть логики записи.

Итоговая схема регистра со сбросом и сигналом разрешения записи:

../.pic/Basic%20Verilog%20structures/registers/fig_05.drawio.svg

Помимо прочего есть еще одно важное правило, которое необходимо знать при описании регистра:

Присваивание регистру может выполняться только в одном блоке always

Даже если вдруг САПР не выдаст сразу сообщение об ошибке, в конечном итоге, на этапе синтеза схемы она рано или поздно появится в виде сообщения связанного с "multiple drivers".

В блоке присваивания регистру можно описывать и комбинационную логику, стоящую перед ним, например схему:

../.pic/Basic%20Verilog%20structures/registers/fig_06.drawio.svg

можно описать как

module reg_example(
  input  logic clk,
  input  logic A,
  input  logic B,
  input  logic reset,
  input  logic enable,
  output logic reg_data
);

  logic reg_name;

  always_ff @(posedge clk) begin
    if(reset) begin
      reg_name <= 1'b0;
    end
    else if(enable) begin
      reg_name <= A & B;
    end
  end

  assign reg_data = reg_name;

endmodule

Однако это всего лишь упрощение. Если вы умеете описывать регистр с подключением к нему всего одного провода на входе данных, вы все равно сможете описать эту схему:

module reg_example(
  input  logic clk,
  input  logic A,
  input  logic B,
  input  logic reset,
  input  logic enable,
  output logic reg_data
);

  logic reg_name;     // Обратите внимание, что несмотря на то, что
  logic ab;           // и reg_name и ab объявлены типом logic,
                      // ab станет проводом, а reg_name - регистром
                      // (из-за непрерывного присваивания на ab, и блока
                      // always_ff для reg_name)

  assign ab = A & B;

  always_ff @(posedge clk) begin
    if(reset) begin
      reg_name <= 1'b0;
    end
    else if(enable) begin
      reg_name <= ab;
    end
  end

  assign reg_data = reg_name;

endmodule

Поэтому так важно разобраться в базовом способе описания регистра.

Более того, с точки зрения синтезатора данное описание проще для синтеза, т.к. ему не разделять из одного always блока комбинационную и синхронные части.

Вообще говоря, регистр в общем смысле этого слова представляет собой многоразрядную конструкцию (в рассмотренном ранее примере, однобитный регистр мог представлять из себя простой D-триггер). Создание многоразрядного регистра мало отличается от создания многоразрядного провода, а описание логики записи в многоразрядный регистр ничем не отличается от логики записи в одноразрядный регистр:

module reg_example(
  input  logic        clk,
  input  logic [7:0]  data,
  output logic [7:0]  reg_data
);

  logic [7:0] reg_name;

  always_ff @(posedge clk) begin
    reg_name <= data;
  end

  assign reg_data = reg_name;

endmodule

Итоги главы

  1. Регистр — это базовая ячейка памяти, позволяющая хранить состояние, пока на схему подается питание.
  2. Для объявления регистра используется тип logic, при необходимости после типа указывается разрядность будущего регистра.
  3. Для описания логики записи в регистр используется блок always_ff, в круглых скобках которого указывается тактирующий сигнал и фронт, по которому будет вестись запись, а также (в случае асинхронного сброса), сигнал сброса.
  4. Регистр может иметь различные управляющие сигналы: установки/сброса/разрешения на запись. Логика этих управляющих сигналов является частью логики записи в этот регистр и так же описывается в блоке always_ff.
  5. При описании логики записи в регистр, необходимо пользоваться оператором неблокирующего присваивания <=.
  6. Нельзя описывать логику записи в регистр более чем в одном блоке always (иными словами, операция присваивания для каждого регистра может находиться только в одном блоке always).

Проверь себя

Как, по-вашему, описать на языке SystemVerilog схему, приведённую ниже?

../.pic/Basic%20Verilog%20structures/registers/fig_07.drawio.svg